import { MultiMap } from 'multi-map'
import { keyNames } from './maps'

// @ts-ignore
// Kludges for bugs and behavior differences that can't be feature
// detected are enabled based on userAgent etc sniffing.
const userAgent = navigator.userAgent
const platform = navigator.platform

const edge = /Edge\/(\d+)/.exec(userAgent)
let webkit = !edge && /WebKit\//.test(userAgent)
const qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent)
let presto = /Opera\//.test(userAgent)
const isSafari = /Apple Computer/.test(navigator.vendor)

const isIos = isSafari && (/Mobile\/\w+/.test(userAgent) || navigator.maxTouchPoints > 2)
// This is woefully incomplete. Suggestions for alternative methods welcome.
const isMac = isIos || /Mac/.test(platform)

let presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/)
// @ts-ignore
if (presto_version) presto_version = Number(presto_version[1])
// @ts-ignore
if (presto_version && presto_version >= 15) {
  presto = false
  webkit = true
}
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
const flipCtrlCmd =
  isMac &&
  (qtwebkit || (presto && (presto_version == null || (presto_version as any as number) < 12.11)))

const buttonStrings = ['Left', 'Middle', 'Right']
const repeatStrings = [undefined, '', 'Double', 'Triple']

export interface ComboKeysOptions {
  noShiftKey?: boolean
  clickInterval?: number
  maxClickInterval?: number
}

type ResolveComboKeysOptions = Required<ComboKeysOptions>

const preventStop = (e: Event) => {
  if (e.preventDefault) {
    e.preventDefault()
  } else {
    e.returnValue = false
  }
  e.stopPropagation()
}

const isPrevent = (e: Event) => {
  if ('defaultPrevented' in e) {
    return e.defaultPrevented
  } else {
    return e.returnValue === false
  }
}

const resolveOptions = (options?: ComboKeysOptions): ResolveComboKeysOptions => {
  return {
    noShiftKey: options?.noShiftKey ?? false,
    clickInterval: options?.clickInterval ?? 200,
    maxClickInterval: options?.maxClickInterval ?? 1000,
  }
}

export class ComboKeys<Args extends any[]> {
  private _opts: ResolveComboKeysOptions
  private _statusMap = new WeakMap<
    EventTarget,
    {
      button: number
      repeat: number
      timeout?: number
      starttime: number
    }
  >()

  eventsMap: MultiMap<string, (e: Event, ...args: Args) => void>

  constructor(options?: ComboKeysOptions)
  constructor(
    entries: Iterable<readonly [string, (e: Event, ...args: Args) => void]> | null,
    options?: ComboKeysOptions
  )

  constructor(entries_or_options?: any, maybe_options?: any) {
    const arg0_is_iterable = typeof entries_or_options?.[Symbol.iterator] === 'function'
    const entries = arg0_is_iterable ? entries_or_options : undefined
    const options = arg0_is_iterable ? maybe_options : entries_or_options
    this._opts = resolveOptions(options)
    this.eventsMap = new MultiMap(entries)
  }

  dispatch(e: MouseEvent | KeyboardEvent, ...args: Args) {
    const { eventsMap, _opts: options, _statusMap: statusMap } = this
    if (!e) return

    const { altKey, metaKey, ctrlKey, shiftKey, currentTarget } = e
    const comboKeys: string[] = []

    let repeat = 1
    let key: string
    if (e instanceof MouseEvent) {
      if (!currentTarget) return
      const { button, timeStamp } = e
      let it:
        | {
            button: number
            repeat: number
            timeout?: number
            starttime: number
          }
        | undefined
      switch (e.type.toLowerCase()) {
        case 'mousedown':
          it = statusMap.get(currentTarget!)
          if (it) {
            clearTimeout(it.timeout)
            it.timeout = undefined
            if (
              timeStamp - it.starttime > options.maxClickInterval ||
              button !== it.button ||
              it.repeat >= 3
            ) {
              it.starttime = timeStamp
              it.repeat = 1
            } else {
              ++it.repeat
            }
          } else {
            statusMap.set(currentTarget!, {
              button: button,
              repeat: 1,
              starttime: e.timeStamp,
            })
          }
          return
        case 'mouseup':
          it = statusMap.get(currentTarget!)
          if (!it) return
          it.button = button
          clearTimeout(it.timeout)
          it.timeout = setTimeout(() => {
            statusMap.delete(currentTarget!)
          }, options.clickInterval)
          repeat = it.repeat
          break
        case 'click':
          if (e.button !== 0) return
          break
        case 'auxclick':
          if (e.button !== 1) return
          break
        case 'contextmenu':
          if (e.button !== 2) return
          break
        case 'dblclick':
          repeat = 2
          break
        default:
          return
      }

      key = buttonStrings[e.button] + repeatStrings[repeat] + 'Click'
    } else {
      key = (keyNames as any)[e.keyCode]
    }

    if (altKey) {
      comboKeys.push('Alt')
    }
    if (flipCtrlCmd ? metaKey : ctrlKey) {
      comboKeys.push('Ctrl')
    }
    if (flipCtrlCmd ? ctrlKey : metaKey) {
      comboKeys.push('Cmd')
    }
    if (!options.noShiftKey && shiftKey) {
      comboKeys.push('Shift')
    }

    let funcs: Iterable<(e: Event, ...args: any) => any> | undefined
    const len = comboKeys.length

    switch (len) {
      case 0:
        funcs = eventsMap.get(key)
        break
      case 1:
        funcs = eventsMap.get(comboKeys[0] + '+' + key)
        break
      case 2:
        funcs =
          eventsMap.get(comboKeys[0] + '+' + comboKeys[1] + '+' + key) ??
          eventsMap.get(comboKeys[1] + '+' + comboKeys[0] + '+' + key)
        break
    }

    let shouldStopAndPrevent = false

    if (funcs) {
      for (const func of funcs) {
        shouldStopAndPrevent = true
        if (isPrevent(e)) break
        func(e, ...args)
      }

      if (!shouldStopAndPrevent && !isPrevent(e)) {
        const currencyFuncs = eventsMap.get('*')
        for (const func of currencyFuncs) {
          shouldStopAndPrevent = true
          if (isPrevent(e)) break
          func(e, ...args)
        }
      }
    }

    if (shouldStopAndPrevent) {
      preventStop(e)
    }
  }
}

export default ComboKeys